﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Brainee.NeuralNetwork
{
  public class BackPropagationTraining2 : ITrainingMethod
  {
    #region Private fields

    private double _learningRate = 0.1;
    private UInt32 _maxIterations = UInt32.MaxValue;
    private double _minError = 0.005;
    private double _momentum = 0.0;

    #endregion
    #region Public properties

    public double LearningRate
    {
      get { return _learningRate; }
      set { _learningRate = Math.Max (0.0, Math.Min (1.0, value)); }
    }

    public UInt32 MaxIterations
    {
      get { return _maxIterations; }
      set { _maxIterations = value; }
    }

    public double MinError
    {
      get { return _minError; }
      set { _minError = value; }
    }

    public double Error { get; private set; }
    
    public UInt32 Iterations { get; private set; }

    public double Momentum
    {
      get { return _momentum; }
      set { _momentum = value; }
    }

    #endregion

    private double[][] neuronErrors;
    private double[][][] weightsUpdates;
    private double[][] thresholdsUpdates;

    public bool Train (Network ann, TrainingSet set)
    {
      // Initialize
      neuronErrors = new double [ann.Count] [];
      weightsUpdates = new double [ann.Count] [][];
      thresholdsUpdates = new double [ann.Count] [];
      for (int i = 0; i < ann.Count; i++)
      {
        neuronErrors [i] = new double [ann [i].OutputCount];
        weightsUpdates [i] = new double [ann [i].OutputCount] [];
        thresholdsUpdates [i] = new double [ann [i].OutputCount];

        for (int j = 0; j < ann [i].OutputCount; j++)
        {
          weightsUpdates [i] [j] = new double [ann [i].InputCount];
        }
      }

      for (Iterations = 0; Iterations < _maxIterations; Iterations++)
      {
        Error = 0;

        // run learning procedure for all samples
        foreach (TrainingItem item in set)
        {
          Error += TrainItem (ann, item);
        }

        if (Progress != null)
        {
          TrainingProgressEventArgs e = new TrainingProgressEventArgs (Iterations, Error);
          Progress (this, e);
          if (e.Cancel)
            break;
        }

        if (Error < MinError)
          break;
      }

      return (Error < MinError);
    }

    private double TrainItem (Network ann, TrainingItem item)
    {
      // compute the network's output
      ann.Compute (item.Input);

      // calculate network error
      double error = CalculateError (ann, item);

      // calculate weights updates
      CalculateUpdates (ann, item);

      // update the network
      UpdateNetwork (ann);

      return error;

    }

    private double CalculateError (Network ann, TrainingItem item)
    {
      // error values
      double error = 0;

      // calculate error values for the last layer first
      Layer	layer = null;
      Layer	layerNext = null;

      double[] errors = null;
      double[] errorsNext = null;

      //for (int neuronIndex = 0; neuronIndex < layer.OutputCount; neuronIndex++)
      //{
      //  // neuron's output value
      //  double output = layer [neuronIndex].Value;

      //  // error of the neuron
      //  double e = item.Output [neuronIndex] - output;

      //  // error multiplied with activation function's derivative
      //  errors [neuronIndex] = e * layer[neuronIndex].Activation.Derivative (output);
        
      //  // square the error and sum it
      //  error += (e * e);
      //}

      // calculate error values for other layers
      for (int layerIndex = ann.Count - 1; layerIndex > 0; layerIndex--)
      {
        layerNext = layer;
        errorsNext = errors;

        layer = ann [layerIndex];
        errors = neuronErrors [layerIndex];

        // for all neurons of the layer
        for (int neuronIndex = 0; neuronIndex < layer.OutputCount; neuronIndex++)
        {
          // neuron's output value
          double output = layer [neuronIndex].Value;

          double delta = 0.0;

          if (layerNext == null)
          {
            delta = item.Output [neuronIndex] - output;

            // square the error and sum it
            error += (delta * delta);
          }
          else
          {
            // for all neurons of the next layer
            for (int k = 0; k < layerNext.OutputCount; k++)
            {
              delta += errorsNext [k] * layerNext [k] [neuronIndex];
            }
          }

          errors [neuronIndex] = delta * layer[neuronIndex].Activation.Derivative (output);

        }
      }

      // return squared error of the last layer divided by 2
      return error / 2.0;
    }

    private void CalculateUpdates (Network ann, TrainingItem item)
    {
      for (int layerIndex = 1; layerIndex < ann.Count; layerIndex++)
      {
        Layer layer = ann [layerIndex];
        Layer layerPrev = layer.Previous;

        double[] errors = neuronErrors [layerIndex];
        double[][] layerWeightsUpdates = weightsUpdates [layerIndex];
        double[] layerThresholdUpdates = thresholdsUpdates [layerIndex];

        // for each neuron of the layer
        for (int i = 0, n = layer.OutputCount; i < n; i++)
        {
          Neuron neuron = layer [i];
          double error = errors [i];
          double[] neuronWeightUpdates = layerWeightsUpdates [i];

          // for each synapse of the neuron
          for (int j = 0; j < neuron.Size; j++)
          {
            // calculate weight update
            neuronWeightUpdates [j] = _learningRate * (
              _momentum * neuronWeightUpdates [j] +
              (1.0 - _momentum) * error * layerPrev [j].Value
              );
          }

          // calculate treshold update
          layerThresholdUpdates [i] = _learningRate * (
            _momentum * layerThresholdUpdates [i] +
            (1.0 - _momentum) * error
            );
        }
      }
    }

    private void UpdateNetwork (Network ann)
    {
      // current neuron
      Neuron	neuron;
      // current layer
      Layer		layer;
      // layer's weights updates
      double[][]	layerWeightsUpdates;
      // layer's thresholds updates
      double[]	layerThresholdUpdates;
      // neuron's weights updates
      double[]	neuronWeightUpdates;

      // for each layer of the network
      for (int i = 1, n = ann.Count; i < n; i++)
      {
        layer = ann [i];
        layerWeightsUpdates = weightsUpdates [i];
        layerThresholdUpdates = thresholdsUpdates [i];

        // for each neuron of the layer
        for (int j = 0, m = layer.OutputCount; j < m; j++)
        {
          neuron = layer [j];
          neuronWeightUpdates = layerWeightsUpdates [j];

          // for each weight of the neuron
          for (int k = 0, s = neuron.Size; k < s; k++)
          {
            // update weight
            neuron [k] += neuronWeightUpdates [k];
          }
          // update treshold
          neuron.Threshold += layerThresholdUpdates [j];
        }
      }
    }

    public event TrainingProgressHandler Progress;
  }
}
